"""Tests for the lifx integration."""

from __future__ import annotations

import asyncio
from contextlib import contextmanager
from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, patch

from aiolifx.aiolifx import Light

from homeassistant.components.lifx import discovery
from homeassistant.components.lifx.const import TARGET_ANY

MODULE = "homeassistant.components.lifx"
MODULE_CONFIG_FLOW = "homeassistant.components.lifx.config_flow"
IP_ADDRESS = "127.0.0.1"
LABEL = "My Bulb"
GROUP = "My Group"
SERIAL = "aa:bb:cc:dd:ee:cc"
MAC_ADDRESS = "aa:bb:cc:dd:ee:cd"
DHCP_FORMATTED_MAC = "aabbccddeecd"
DEFAULT_ENTRY_TITLE = LABEL


class MockMessage:
    """Mock a lifx message."""

    def __init__(self, **kwargs: Any) -> None:
        """Init message."""
        self.target_addr = SERIAL
        self.count = 9
        for k, v in kwargs.items():
            if k != "callb":
                setattr(self, k, v)


class MockFailingLifxCommand:
    """Mock a lifx command that fails."""

    def __init__(self, bulb, **kwargs: Any) -> None:
        """Init command."""
        self.bulb = bulb
        self.calls = []

    def __call__(self, *args, **kwargs):
        """Call command."""
        if callb := kwargs.get("callb"):
            callb(self.bulb, None)
        self.calls.append([args, kwargs])

    def reset_mock(self):
        """Reset mock."""
        self.calls = []


class MockLifxCommand:
    """Mock a lifx command."""

    def __name__(self):
        """Return name."""
        return "mock_lifx_command"

    def __init__(self, bulb, **kwargs: Any) -> None:
        """Init command."""
        self.bulb = bulb
        self.calls = []
        self.msg_kwargs = {
            k.removeprefix("msg_"): v for k, v in kwargs.items() if k.startswith("msg_")
        }
        for k, v in kwargs.items():
            if k.startswith("msg_") or k == "callb":
                continue
            setattr(self.bulb, k, v)

    def __call__(self, *args, **kwargs):
        """Call command."""
        if callb := kwargs.get("callb"):
            callb(self.bulb, MockMessage(**self.msg_kwargs))
        self.calls.append([args, kwargs])

    def reset_mock(self):
        """Reset mock."""
        self.calls = []


def _mocked_bulb() -> Light:
    bulb = Light(asyncio.get_running_loop(), SERIAL, IP_ADDRESS)
    bulb.host_firmware_version = "3.00"
    bulb.label = LABEL
    bulb.group = GROUP
    bulb.color = [1, 2, 3, 4]
    bulb.power_level = 0
    bulb.fire_and_forget = AsyncMock()
    bulb.set_reboot = Mock()
    bulb.try_sending = AsyncMock()
    bulb.set_infrared = MockLifxCommand(bulb)
    bulb.get_label = MockLifxCommand(bulb)
    bulb.get_group = MockLifxCommand(bulb)
    bulb.get_color = MockLifxCommand(bulb)
    bulb.set_power = MockLifxCommand(bulb)
    bulb.set_color = MockLifxCommand(bulb)
    bulb.get_hostfirmware = MockLifxCommand(bulb)
    bulb.get_wifiinfo = MockLifxCommand(bulb, signal=100)
    bulb.get_version = MockLifxCommand(bulb)
    bulb.set_waveform_optional = MockLifxCommand(bulb)
    bulb.product = 1  # LIFX Original 1000
    return bulb


def _mocked_failing_bulb() -> Light:
    bulb = _mocked_bulb()
    bulb.get_color = MockFailingLifxCommand(bulb)
    bulb.set_power = MockFailingLifxCommand(bulb)
    bulb.set_color = MockFailingLifxCommand(bulb)
    bulb.get_hostfirmware = MockFailingLifxCommand(bulb)
    bulb.get_version = MockFailingLifxCommand(bulb)
    return bulb


def _mocked_white_bulb() -> Light:
    bulb = _mocked_bulb()
    bulb.product = 19  # LIFX White 900 BR30 (High Voltage)
    return bulb


def _mocked_brightness_bulb() -> Light:
    bulb = _mocked_bulb()
    bulb.product = 51  # LIFX Mini White
    return bulb


def _mocked_clean_bulb() -> Light:
    bulb = _mocked_bulb()
    bulb.get_hev_cycle = MockLifxCommand(bulb)
    bulb.set_hev_cycle = MockLifxCommand(bulb)
    bulb.get_hev_configuration = MockLifxCommand(bulb)
    bulb.get_last_hev_cycle_result = MockLifxCommand(bulb)
    bulb.hev_cycle_configuration = {"duration": 7200, "indication": False}
    bulb.hev_cycle = {
        "duration": 7200,
        "remaining": 30,
        "last_power": False,
    }
    bulb.last_hev_cycle_result = 0
    bulb.product = 90
    return bulb


def _mocked_infrared_bulb() -> Light:
    bulb = _mocked_bulb()
    bulb.product = 29  # LIFX A19 Night Vision
    bulb.infrared_brightness = 65535
    bulb.set_infrared = MockLifxCommand(bulb)
    bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=65535)
    return bulb


def _mocked_light_strip() -> Light:
    bulb = _mocked_bulb()
    bulb.product = 31  # LIFX Z
    bulb.zones_count = 3
    bulb.color_zones = [MagicMock()] * 3
    bulb.effect = {"effect": "MOVE", "speed": 3, "duration": 0, "direction": "RIGHT"}
    bulb.get_color_zones = MockLifxCommand(
        bulb,
        msg_seq_num=bulb.seq_next(),
        msg_count=bulb.zones_count,
        msg_index=0,
        msg_color=bulb.color_zones,
    )
    bulb.set_color_zones = MockLifxCommand(bulb)
    bulb.get_multizone_effect = MockLifxCommand(bulb)
    bulb.set_multizone_effect = MockLifxCommand(bulb)
    bulb.get_extended_color_zones = MockLifxCommand(bulb)
    bulb.set_extended_color_zones = MockLifxCommand(bulb)
    return bulb


def _mocked_tile() -> Light:
    bulb = _mocked_bulb()
    bulb.product = 55  # LIFX Tile
    bulb.effect = {"effect": "OFF"}
    bulb.get_tile_effect = MockLifxCommand(bulb)
    bulb.set_tile_effect = MockLifxCommand(bulb)
    bulb.get64 = MockLifxCommand(bulb)
    bulb.get_device_chain = MockLifxCommand(bulb)
    return bulb


def _mocked_ceiling() -> Light:
    bulb = _mocked_bulb()
    bulb.product = 176  # LIFX Ceiling
    bulb.effect = {"effect": "OFF"}
    bulb.get_tile_effect = MockLifxCommand(bulb)
    bulb.set_tile_effect = MockLifxCommand(bulb)
    bulb.get64 = MockLifxCommand(bulb)
    bulb.get_device_chain = MockLifxCommand(bulb)
    return bulb


def _mocked_128zone_ceiling() -> Light:
    bulb = _mocked_bulb()
    bulb.product = 201  # LIFX 26"x13" Ceiling
    bulb.effect = {"effect": "OFF"}
    bulb.get_tile_effect = MockLifxCommand(bulb)
    bulb.set_tile_effect = MockLifxCommand(bulb)
    bulb.get64 = MockLifxCommand(bulb)
    bulb.get_device_chain = MockLifxCommand(bulb)
    return bulb


def _mocked_bulb_old_firmware() -> Light:
    bulb = _mocked_bulb()
    bulb.host_firmware_version = "2.77"
    return bulb


def _mocked_bulb_new_firmware() -> Light:
    bulb = _mocked_bulb()
    bulb.host_firmware_version = "3.90"
    return bulb


def _mocked_relay() -> Light:
    bulb = _mocked_bulb()
    bulb.product = 70  # LIFX Switch
    return bulb


def _patch_device(device: Light | None = None, no_device: bool = False):
    """Patch out discovery."""

    class MockLifxConnecton:
        """Mock lifx discovery."""

        def __init__(self, *args: Any, **kwargs: Any) -> None:
            """Init connection."""
            if no_device:
                self.device = _mocked_failing_bulb()
            else:
                self.device = device or _mocked_bulb()
            self.device.mac_addr = TARGET_ANY

        async def async_setup(self):
            """Mock setup."""

        def async_stop(self):
            """Mock teardown."""

    @contextmanager
    def _patcher():
        with patch("homeassistant.components.lifx.LIFXConnection", MockLifxConnecton):
            yield

    return _patcher()


def _patch_discovery(device: Light | None = None, no_device: bool = False):
    """Patch out discovery."""

    class MockLifxDiscovery:
        """Mock lifx discovery."""

        def __init__(self, *args: Any, **kwargs: Any) -> None:
            """Init discovery."""
            if no_device:
                self.lights = {}
                return
            discovered = device or _mocked_bulb()
            self.lights = {discovered.mac_addr: discovered}

        def start(self):
            """Mock start."""

        def cleanup(self):
            """Mock cleanup."""

    @contextmanager
    def _patcher():
        with (
            patch.object(discovery, "DEFAULT_TIMEOUT", 0),
            patch(
                "homeassistant.components.lifx.discovery.LifxDiscovery",
                MockLifxDiscovery,
            ),
        ):
            yield

    return _patcher()


def _patch_config_flow_try_connect(
    device: Light | None = None, no_device: bool = False
):
    """Patch out discovery."""

    class MockLifxConnection:
        """Mock lifx discovery."""

        def __init__(self, *args: Any, **kwargs: Any) -> None:
            """Init connection."""
            if no_device:
                self.device = _mocked_failing_bulb()
            else:
                self.device = device or _mocked_bulb()
            self.device.mac_addr = TARGET_ANY

        async def async_setup(self):
            """Mock setup."""

        def async_stop(self):
            """Mock teardown."""

    @contextmanager
    def _patcher():
        with patch(
            "homeassistant.components.lifx.config_flow.LIFXConnection",
            MockLifxConnection,
        ):
            yield

    return _patcher()
